//
//  YRNKDataStore.swift
//  YoruNeko
//
//  Created by liphone on 2021/6/17.
//

import Foundation

/**
 此类用于存取json对象
 */

class YRNKDataStore {
    

    func writeToStorage(user_uuid:UUID,srcType: DataSource,jsonObj:JSON)->Bool{
       
        //open a filehandle for write
        if let file = getFileHandle(for: user_uuid, of: srcType,rwOption: .Writing){
            if let data = try? jsonObj.rawData() {
                YRNKLog(self.description, msg: "\(user_uuid.uuidString)写入文件...")
                file.write(data)
                file.closeFile()
                YRNKLog(self.description, msg: "\(user_uuid.uuidString)写入完成")
                return true
            }
        }
        //data write failed
        return false;
    }
    
    func readFromStorage(user_uuid:UUID,srcType: DataSource)->JSON?{
        if let file = getFileHandle(for: user_uuid, of: srcType,rwOption: .Reading){
            if let data = try? file.readToEnd(){
                YRNKLog(self.description, msg: "\(user_uuid.uuidString)读取文件...")
                file.closeFile()
                return try? JSON.init(data: data)
                
            }
        }
        YRNKLog(description, msg: "读取文件失败，可能是没有获取到文件句柄")
        return nil
    }
    
    private func getFileHandle(for user: UUID, of srcType:DataSource,rwOption:ReadWriteOption)->FileHandle?{
        
        //construct url
        var baseUrl = getDocumentURL()
        baseUrl.appendPathComponent("d_\(user.uuidString)", isDirectory: true)
        
        //判断文件夹是否存在
        if !fileManager.fileExists(atPath: baseUrl.path){
            YRNKLog(description, msg: "文件夹不存在，准备创建")
            //创建文件夹
            do {
                try fileManager.createDirectory(at: baseUrl, withIntermediateDirectories: true, attributes:.none)
                YRNKLog(description, msg: "创建文件夹："+baseUrl.path)
            } catch {
                YRNKLog(description, msg: "创建文件夹："+baseUrl.path+" 失败")
                return nil
            }

        }
        
        let fileUrl = baseUrl.appendingPathComponent(srcType.rawValue+"_"+user.uuidString, conformingTo: .json)
            
        var file:FileHandle?
        switch rwOption {
        case .Reading:
            file  = FileHandle.init(forReadingAtPath: fileUrl.path)
        case .Writing:
            //判断是否存在,不存在且创建失败则break
            if !existAt(url: fileUrl) && !fileManager.createFile(atPath: fileUrl.path, contents: nil, attributes: .none){
                YRNKLog(description, msg: "文件\(fileUrl.path)不存在，且创建失败")
                break;
            }
            YRNKLog(description, msg: "初始化文件\(fileUrl.path)")
            file = FileHandle.init(forWritingAtPath: fileUrl.path)
        case .Updating:
            if !existAt(url: fileUrl) && !fileManager.createFile(atPath: fileUrl.path, contents: nil, attributes: .none){
                YRNKLog(description, msg: "文件\(fileUrl.path)不存在，且创建失败")
                break;
            }
            YRNKLog(description, msg: "初始化文件\(fileUrl.path)")
            file = FileHandle.init(forUpdatingAtPath: fileUrl.path)
        }
        return file;
    }
    
    
    func existAt(url:URL)->Bool{
        return fileManager.fileExists(atPath: url.path)
    }
    
    func getDocumentURL() -> URL{
        return try! fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
    }
    
    private let fileManager = FileManager.default
    private let description = "YRNKDataStore"
    
    enum DataSource:String{
        case history = "history"
        case bookmark = "bookmark"
    }
    
    enum YRNKDataStoreError:Error{
        case CouldNotCreateFileHandle
    }
    
    private enum ReadWriteOption{
        case Reading,Writing,Updating
    }
}

/*
 some helpful log:
 print(FileManager.default.urls(for: .userDirectory, in: .userDomainMask))
 print(src.urls(for: .documentDirectory
                , in: .allDomainsMask))
 print(src.urls(for: .userDirectory, in: .allDomainsMask))
 
 YRNKLog("urls: localDomainMask ", msg: src.urls(for: .documentDirectory, in: .localDomainMask).debugDescription)
 [file:///Users/liphone/Library/Developer/CoreSimulator/Devices/566A3941-423C-4232-8363-291796A3C0C6/data/Containers/Data/Application/E9EDCBFC-96EE-4F12-B854-FE04CE31DE11/Documents/]
 [file:///Users/, file:///Network/Users/]
 [urls: localDomainMask ]:[]
 */

/**
 help message:
 Usage
 Initialization
 import SwiftyJSON
 let json = JSON(data: dataFromNetworking)
 Or

 let json = JSON(jsonObject)
 Or

 if let dataFromString = jsonString.data(using: .utf8, allowLossyConversion: false) {
     let json = JSON(data: dataFromString)
 }
 Subscript
 // Getting a double from a JSON Array
 let name = json[0].double
 // Getting an array of string from a JSON Array
 let arrayNames =  json["users"].arrayValue.map {$0["name"].stringValue}
 // Getting a string from a JSON Dictionary
 let name = json["name"].stringValue
 // Getting a string using a path to the element
 let path: [JSONSubscriptType] = [1,"list",2,"name"]
 let name = json[path].string
 // Just the same
 let name = json[1]["list"][2]["name"].string
 // Alternatively
 let name = json[1,"list",2,"name"].string
 // With a hard way
 let name = json[].string
 // With a custom way
 let keys:[JSONSubscriptType] = [1,"list",2,"name"]
 let name = json[keys].string
 Loop
 // If json is .Dictionary
 for (key,subJson):(String, JSON) in json {
    // Do something you want
 }
 The first element is always a String, even if the JSON is an Array

 // If json is .Array
 // The `index` is 0..<json.count's string value
 for (index,subJson):(String, JSON) in json {
     // Do something you want
 }
 Error
 SwiftyJSON 4.x
 SwiftyJSON 4.x introduces an enum type called SwiftyJSONError, which includes unsupportedType, indexOutOfBounds, elementTooDeep, wrongType, notExist and invalidJSON, at the same time, ErrorDomain are being replaced by SwiftyJSONError.errorDomain. Note: Those old error types are deprecated in SwiftyJSON 4.x and will be removed in the future release.

 SwiftyJSON 3.x
 Use a subscript to get/set a value in an Array or Dictionary

 If the JSON is:

 an array, the app may crash with "index out-of-bounds."
 a dictionary, it will be assigned to nil without a reason.
 not an array or a dictionary, the app may crash with an "unrecognised selector" exception.
 This will never happen in SwiftyJSON.

 let json = JSON(["name", "age"])
 if let name = json[999].string {
     // Do something you want
 } else {
     print(json[999].error!) // "Array[999] is out of bounds"
 }
 let json = JSON(["name":"Jack", "age": 25])
 if let name = json["address"].string {
     // Do something you want
 } else {
     print(json["address"].error!) // "Dictionary["address"] does not exist"
 }
 let json = JSON(12345)
 if let age = json[0].string {
     // Do something you want
 } else {
     print(json[0])       // "Array[0] failure, It is not an array"
     print(json[0].error!) // "Array[0] failure, It is not an array"
 }

 if let name = json["name"].string {
     // Do something you want
 } else {
     print(json["name"])       // "Dictionary[\"name"] failure, It is not an dictionary"
     print(json["name"].error!) // "Dictionary[\"name"] failure, It is not an dictionary"
 }
 Optional getter
 // NSNumber
 if let id = json["user"]["favourites_count"].number {
    // Do something you want
 } else {
    // Print the error
    print(json["user"]["favourites_count"].error!)
 }
 // String
 if let id = json["user"]["name"].string {
    // Do something you want
 } else {
    // Print the error
    print(json["user"]["name"].error!)
 }
 // Bool
 if let id = json["user"]["is_translator"].bool {
    // Do something you want
 } else {
    // Print the error
    print(json["user"]["is_translator"].error!)
 }
 // Int
 if let id = json["user"]["id"].int {
    // Do something you want
 } else {
    // Print the error
    print(json["user"]["id"].error!)
 }
 ...
 Non-optional getter
 Non-optional getter is named xxxValue

 // If not a Number or nil, return 0
 let id: Int = json["id"].intValue
 // If not a String or nil, return ""
 let name: String = json["name"].stringValue
 // If not an Array or nil, return []
 let list: Array<JSON> = json["list"].arrayValue
 // If not a Dictionary or nil, return [:]
 let user: Dictionary<String, JSON> = json["user"].dictionaryValue
 Setter
 json["name"] = JSON("new-name")
 json[0] = JSON(1)
 json["id"].int =  1234567890
 json["coordinate"].double =  8766.766
 json["name"].string =  "Jack"
 json.arrayObject = [1,2,3,4]
 json.dictionaryObject = ["name":"Jack", "age":25]
 Raw object
 let rawObject: Any = json.object
 let rawValue: Any = json.rawValue
 //convert the JSON to raw NSData
 do {
     let rawData = try json.rawData()
   //Do something you want
 } catch {
     print("Error \(error)")
 }
 //convert the JSON to a raw String
 if let rawString = json.rawString() {
   //Do something you want
 } else {
     print("json.rawString is nil")
 }
 Existence
 // shows you whether value specified in JSON or not
 if json["name"].exists()
 Literal convertibles
 For more info about literal convertibles: Swift Literal Convertibles

 // StringLiteralConvertible
 let json: JSON = "I'm a json"
 / /IntegerLiteralConvertible
 let json: JSON =  12345
 // BooleanLiteralConvertible
 let json: JSON =  true
 // FloatLiteralConvertible
 let json: JSON =  2.8765
 // DictionaryLiteralConvertible
 let json: JSON =  ["I":"am", "a":"json"]
 // ArrayLiteralConvertible
 let json: JSON =  ["I", "am", "a", "json"]
 // With subscript in array
 var json: JSON =  [1,2,3]
 json[0] = 100
 json[1] = 200
 json[2] = 300
 json[999] = 300 // Don't worry, nothing will happen
 // With subscript in dictionary
 var json: JSON =  ["name": "Jack", "age": 25]
 json["name"] = "Mike"
 json["age"] = "25" // It's OK to set String
 json["address"] = "L.A." // Add the "address": "L.A." in json
 // Array & Dictionary
 var json: JSON =  ["name": "Jack", "age": 25, "list": ["a", "b", "c", ["what": "this"]]]
 json["list"][3]["what"] = "that"
 json["list",3,"what"] = "that"
 let path: [JSONSubscriptType] = ["list",3,"what"]
 json[path] = "that"
 // With other JSON objects
 let user: JSON = ["username" : "Steve", "password": "supersecurepassword"]
 let auth: JSON = [
   "user": user.object, // use user.object instead of just user
   "apikey": "supersecretapitoken"
 ]
 Merging
 It is possible to merge one JSON into another JSON. Merging a JSON into another JSON adds all non existing values to the original JSON which are only present in the other JSON.

 If both JSONs contain a value for the same key, mostly this value gets overwritten in the original JSON, but there are two cases where it provides some special treatment:

 In case of both values being a JSON.Type.array the values form the array found in the other JSON getting appended to the original JSON's array value.
 In case of both values being a JSON.Type.dictionary both JSON-values are getting merged the same way the encapsulating JSON is merged.
 In a case where two fields in a JSON have different types, the value will get always overwritten.

 There are two different fashions for merging: merge modifies the original JSON, whereas merged works non-destructively on a copy.

 let original: JSON = [
     "first_name": "John",
     "age": 20,
     "skills": ["Coding", "Reading"],
     "address": [
         "street": "Front St",
         "zip": "12345",
     ]
 ]

 let update: JSON = [
     "last_name": "Doe",
     "age": 21,
     "skills": ["Writing"],
     "address": [
         "zip": "12342",
         "city": "New York City"
     ]
 ]

 let updated = original.merge(with: update)
 // [
 //     "first_name": "John",
 //     "last_name": "Doe",
 //     "age": 21,
 //     "skills": ["Coding", "Reading", "Writing"],
 //     "address": [
 //         "street": "Front St",
 //         "zip": "12342",
 //         "city": "New York City"
 //     ]
 // ]
 String representation
 There are two options available:

 use the default Swift one
 use a custom one that will handle optionals well and represent nil as "null":
 let dict = ["1":2, "2":"two", "3": nil] as [String: Any?]
 let json = JSON(dict)
 let representation = json.rawString(options: [.castNilToNSNull: true])
 // representation is "{\"1\":2,\"2\":\"two\",\"3\":null}", which represents {"1":2,"2":"two","3":null}
 */
